!function ($) {

  'use strict'; 
  
  var $validator
  , locale = $('html').prop('lang')
  , errorMessages
  , prototypeOverride = {
  	elements: function () {
  		var validator = this
  		, rulesCache = {}
  		, customFilter = function () {
  			var name = this.name

  			!name && validator.settings.debug && window.console && console.error('%o has no name assigned', this)

                // Custom check to see if field is both hidden AND 1. inside hidden bootstrap tab 2. OR NOT contained within a Bootstrap element
                if ($(this).is(':hidden')) {
                	if ($(this).parents('.tab-pane:not(.active)')[0]) {
                		return false
                	}
                	if (!$(this).parents('.btn-group')[0] || $(this).parents('.btn-group').is(':hidden')) {
                		return false
                	}
                }
                if ($(this).parents('.collapse:not(.in)')[0]) {
                	return false
                }

                if (name in rulesCache || !validator.objectLength($(this).rules())) { // select only the first element for each name, and only those with rules specified
                	return false
                }

                rulesCache[name] = true

                return true
              }

          // select all valid inputs inside the form (no submit or reset buttons)
          return $(this.currentForm)
          .find('input, select, textarea')
          .not(':submit, :reset, :image, :disabled') // Removed :hidden from list, it's accounted for in the custom filter function -
          .not(this.settings.ignore)
          .filter(customFilter)
        }

        , check: function (element) {

        	element = this.validationTargetFor(this.clean(element))

        	var rules = $(element).rules()
        	, dependencyMismatch = false
        	, val = this.elementValue(element)
        	, result

        	for (var method in rules) {
        		var rule = { method: method, parameters: rules[method] }
        		try {
        			result = $.validator.methods[method].call(this, val, element, rule.parameters)

              // if a method indicates that the field is optional and therefore valid,
              // don't mark it as valid when there are no other rules
              if (result === 'dependency-mismatch') {
              	dependencyMismatch = true
              	continue
              }
              dependencyMismatch = false

              if (result === 'pending') {
              	this.toHide = this.toHide.not(this.errorsFor(element))
              	return
              }

              if (!result) {
              	this.formatAndAdd(element, rule)
              	return false
              }
            } catch (e) {
            	if (this.settings.debug && window.console) {
            		console.log('Exception occurred when checking element ' + element.id + ', check the \'' + rule.method + '\' method.', e)
            	}
            	throw e
            }
          }
          if (dependencyMismatch) {
          	return
          }
          if (this.objectLength(rules)) {
          	this.successList.push(element)
          }

          // $(element)
          // .parents('.control-group')
          // .addClass(this.settings.validClass)
          // .removeClass(this.settings.errorClass); //@edit: adds 'success' class, removes 'error' classes to parent container -> @snypelife

          return true
        }

        , showLabel: function (element, message) {
        	var label = this.errorsFor(element)

          // $(element)
          // .parents('.control-group')
          // .addClass(this.settings.errorClass)
          // .removeClass(this.settings.validClass) //@edit: applies error classes to parent container -> @snypelife

          if (label.length) {
            // refresh error/success class
            label.removeClass(this.settings.validClass).addClass(this.settings.errorClass)
            // replace message on existing label
            label.html(message)
          } else {
            // create label
            label = $('<' + this.settings.errorElement + '>')
            .attr('for', this.idOrName(element))
              .addClass(this.settings.errorClass)
              .html(message || '')
              if (this.settings.wrapper) {
              // make sure the element is visible, even in IE
              // actually showing the wrapped element is handled elsewhere
              label = label.hide().show().wrap('<' + this.settings.wrapper + '/>').parent()
            }
            if (!this.labelContainer.append(label).length) {
            	if (this.settings.errorPlacement) {
            		this.settings.errorPlacement(label, $(element))
            	} else {
            		if (element.type === 'radio') {
            			$('[for=' + element.id + ']').append(label)
            		}
            		else {
                  console.log($(element.parentNode).children().length)
                  // Defect: error message breaks BS input-prepend/append
                  // Solution: add conditonal to insert error message prepend/append parent
            			label.insertAfter(
                    /input-prepend|input-append/.test(element.parentNode.className) ?
                    element.parentNode :
                    $(element.parentNode).children().length > 1 ?
                      element.parentNode.lastChild :
                      element)
            		}
            	}
            }
          }
          if (!message && this.settings.success) {
          	label.text('')
          	if (typeof this.settings.success === 'string') {
          		label.addClass(this.settings.success)
          	} else {
          		this.settings.success(label, element)
          	}
          }
          this.toShow = this.toShow.add(label)
        }

        , focusInvalid: function() {
        	var $el
        	if ( this.settings.focusInvalid ) {
        		try {
        			$el = $(this.findLastActive() || this.errorList.length && this.errorList[0].element || [])

        			if ($el.is(':hidden')) {
        				$('html,body,.view').scrollTop($el.siblings().offset().top)
        			}

        			$el
        			.filter(":visible")
        			.focus()
        			.trigger("focusin")
        		} catch(e) {
              // ignore IE throwing errors when focusing hidden elements
            }
          }
        }
      }




      , methodsOverride = {
      	required: function (value, element, param) {
          if (!this.depend(param, element)) {// check if dependency is met
          	return 'dependency-mismatch'
          }
          switch (element.nodeName.toLowerCase()) {
          	case 'select':
                // could be an array for select-multiple or a string, both are fine this way
                var val = typeof $(element).val() === 'object' ? value : $(element).val() // check if jQuery value  is  considered as an object
                return element.disabled || (!!val && !!val.length) // Smarter value checking
                case 'input':
                if (this.checkable(element)) {
                	return this.getLength(value, element) > 0
                }
                default:
                return $.trim(value).length > 0
              }
            }

      , strict: function (value, element) {// Makes field strictly required, no matter what (i.e. even disabled fields).
      	var val = $(element).val()
      	return (val && val.length > 0 && val !== '' && val !== ' ' && val !== null)
      }

      , equalTo: function (value, element) {// equal to password.
        	var val = $(element).val()
        	return val === $("#password").val()
        }
      
      , alphanumeric: function (value, element) { //Checks that field contains only numbers and letters
      	return this.optional(element) || /^[\d\w]+$/.test(value)
      }

        // Checks all fields within a group so that either all fields are filled or none are filled out
        , group: function (value, element, param) {
        	var $param
        	, val = value || ''
        	, isValid = true

        	if (/\#/.test(param)) {
        		$param = $(param).find('input, select')
        	}
        	else if (/\./.test(param)){
        		$param = $(param).length > 1 ? $(param) : $(param).find('input:not(:hidden), select')
        	}
        	else {
        		return true
        	}

        	for (var x = 0; x < $param.length; x += 1) {
        		if ($param[x].id !== element.id) {
        			if (val.length < 1 && $param[x].value.length > 0) {
        				isValid = false
        				break
        			} else {
        				isValid = true
        			}
        		}
        	}
        	return isValid
        }

      , date: function (value, element) { // Custom date rule
      	var str = (value.replace(/[\-\/]/g, '')).match(/^(0+[1-9]|[12][0-9]|3[01])(0+[1-9]|1[012])(\d{4})$/)
      	, valid = /^(0[1-9]|[12][0-9]|3[01])[\/](0[1-9]|1[012])[\/](\d{4})$/.test(value)

      	if (typeof(value) !== 'undefined' && str !== null && !valid) {
      		element.value = value = str[1]+'/'+str[2]+'/'+str[3]
      	}
      	return this.optional(element) || /^(0[1-9]|[12][0-9]|3[01])[\/](0[1-9]|1[012])[\/](\d{4}$)/.test(value)
      }

      , _name: function (value, element) { // Accepted characters in names
      	return this.optional(element) || /[^\d~!@#$%\^&*()_=+\/{}\\\\\\|\\\[\\\],']+$/.test(value)
      }

      , norepeat: function (value, element) { // Checks if value is a repeated digit (e.g. 111111)
      	if (!value) {
      		return
      	}
      	return this.optional(element) || (!/\b(\d)\1+\b/.test(value))
      }

      , nosequential: function (value, element) { // Checks if value is in sequential order (e.g. 123456)
      	if (!value) {
      		return
      	}
      	var val = value.match(/(\d)/g)
      	, isSequential = false

      	for (var i = 0; i < val.length; i += 1) {
      		val[i] = parseInt(val[i], 10)
      		if (i !== 0) {
      			if (val[i] === val[i-1] + 1) {
      				isSequential = true
      			} else {
      				isSequential = false
      				break
      			}
      		}
      	}
      	return this.optional(element) || !isSequential
      }

      , phonenumber: function (value, element) { // Checks if value follows valid phone number, then formats it (e.g. (xxx) xxx-xxxx)
      	var num = value.match(/^(1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})(\s?-?[2-9]\d{2})(\s?-?\d{4})$/)
      	, valid = /^(\([2-9]\d{2}\))(\s[2-9]\d{2})(-\d{4})$/.test(value)

      	if (typeof(value) !== 'undefined' && num !== null && !valid) {
      		element.value = value = '(' + num[2].replace(/[(|)]/g,'') + ') ' + num[3].replace(/\s|-/g,'') + '-' + num[4].replace(/\s|-/g,'')
      	}

      	return this.optional(element) || /^(\([2-9]\d{2}\))(\s[2-9]\d{2})(-\d{4})$/.test(value);
      }

      , postalcode: function (value, element) { // Checks for valid postal code, then formats it (e.g. A1A 1A1)
      	var pCode = value.match(/^([ABCEGHJKLMNPRSTVXY]{1}\d{1}[A-z]{1})(?:\s?)(\d{1}[A-z]{1}\d{1})$/i)
      	, valid = /^[ABCEGHJKLMNPRSTVXY]{1}\d{1}[A-Z]{1} *\d{1}[A-Z]{1}\d{1}$/g.test(value)

      	if (typeof(value) !== 'undefined' && pCode !== null && !valid) {
      		element.value = value = pCode[1].toUpperCase()+' '+pCode[2].toUpperCase()
      	}
      	return this.optional(element) || /^[ABCEGHJKLMNPRSTVXY]{1}\d{1}[A-Z]{1} *\d{1}[A-Z]{1}\d{1}$/g.test(value)
      }

      , postalZip: function (value, element) { // Checks for both postal codes and Zip codes (e.g. A1A 1A1 || 90210)
      	return this.optional(element) || /(^\d{5}(-\d{4})?$)|(^[ABCEGHJKLMNPRSTVXY]{1}\d{1}[A-z]{1} *\d{1}[A-Z]{1}\d{1}$)/gi.test(value)
      }

      , charOnly: function (value, element) { // Checks that value only contains letters
      	return this.optional(element) || /^[A-z]+$/gi.test(value)
      }

      , minAge: function (value, element, param) { // Checks if value meets the minimum age parameter
      	var age = new Date(value.substr(6, 4), value.substr(3, 2) - 1, value.substr(0, 2))
      	, d = new Date()
      	, minAge = new Date((d.getFullYear() - param), d.getMonth(), d.getDate() + 1)

      	return this.optional(element) || (age.getTime() < minAge.getTime())
      }

      , maxAge: function (value, element, param) { // Checks if value meets the maximum age parameter
      	var age = new Date(value.substr(6, 4), parseInt(value.substr(3, 2), 10) - 1, value.substr(0, 2))
      	, d = new Date()
      	, maxAge = new Date((d.getFullYear() - param), d.getMonth(), d.getDate() + 1)

      	return this.optional(element) || (age.getTime() > maxAge.getTime())
      }

      , currency: function (value, element) { // Checks that value is in a currency format
          var locale = $('html').prop('lang')
          var ZP_FORM_CURRENCY_REGEX = new RegExp("(^\\d*(\\.\\d{1,2})?$)|(^[1-9]{1}(,\\d{3})*(\\.\\d{1,2})?$)|(^[1-9]{1}\\d{1,2}(,\\d{3})*(\\.\\d{1,2})?$)");
          if (locale === 'fr-CA') {
            ZP_FORM_CURRENCY_REGEX = new RegExp("(^\\d*(\\,\\d{1,2})?$)|(^[1-9]{1}(\\s\\d{3})*(\\,\\d{1,2})?$)|(^[1-9]{1}\\d{1,2}(\\s\\d{3})*(\\,\\d{1,2})?$)");
          }
          return this.optional( element ) || ZP_FORM_CURRENCY_REGEX.test( value );
        }

        //Consolidated ASP - customize min function to convert value to a proper number
        , min: function (value, element, param) {
        	var val = value.replace(/((?:[\,\s]?\d{0,3})*)([\,\.]\d{2})/g, function (input, dollar, cents) {
        		return dollar.replace(/[\,\s]/g, '') + cents.replace(/[\,]/, '.')
        	})
        	return this.optional(element) || parseFloat(val) >= parseFloat(param)
        }

        // Custom max validation rule - @snypelife
        , max: function (value, element, param) {
        	var val = value.replace(/((?:[\,\s]?\d{0,3})*)([\,\.]\d{2})/g, function (input, dollar, cents) {
        		return dollar.replace(/[\,\s]/g, '') + cents.replace(/[\,]/, '.')
        	})
        	return this.optional(element) || parseFloat(val) <= parseFloat(param)
        }
        //Checks that value is beyond today's date
        , todate: function (value, element) {
        	var future = new Date(value.substr(6, 4), value.substr(3, 2) - 1, value.substr(0, 2))
        	, d = new Date()

        	return this.optional(element) || (future.getTime() > d.getTime())
        }

        //Checks that the agreement field has been checked
        , agree: function (value, element) {
        	var val = $(element).val()
        	return val && (val.toLowerCase() === 'yes' || val.toLowerCase() === 'true' || val.toLowerCase() === 'y')
        }

        //Checks that atleast one field within the class group is filled in
        , oneRequired: function (value, element, param) {
        	var param = $(param)
        	, val = value
        	, isValid = true

        	val = (typeof(val) !== 'undefined') ? val : ''
        	for (var x = 0; x < param.length; x += 1) {
        		if (param[x].value.length > 0) {
        			isValid = true
        			break
        		} else {
        			isValid = false
        		}
        	}

        	return isValid
        }

        , startDate: function (value, element, param) {
        	var offset = param * 8.64e7
        	, input = value.split('/')

        	return this.optional(element) || +new Date(input[2], parseInt(input[1], 10) - 1, input[0]) > (+new Date() + offset)
        }

        , endDate: function (value, element, param) {
        	var offset = param * 8.64e7
        	, input = value.split('/')

        	return this.optional(element) || +new Date(input[2], parseInt(input[1], 10) - 1, input[0]) < (+new Date() + offset)
        }

      // Checks that value is in acceptable list provided
      , list: function (value, element, param) {
      	return this.optional(element) || $.inArray(value, param) !== -1
      }
    }

    , extractRules = function () {  // Extracts validation rules from HTML data-rules attrbutes, parses and sets in an object
    	var rules = {}

    	$('[data-rules]').each(function () {
    		var $this = $(this)
    		, id = $this.prop('id')
    		, ruleMap = $this.attr('data-rules').split(',')
    		, rule

    		if (!id) {
    			return
    		}

    		rules[id] = {}


    		for (var i = 0; i < ruleMap.length; i += 1) {
    			rule = ruleMap[i].split(':')

          if (/true|false/.test(rule[1])) { // String to Boolean check
          	rule[1] = rule[1] === 'true' ? true : false
          }

          else if (/^[\"\']*((?:[\,\s]?[\-]?\d{0,3})*)([\,\.]\d{2})[\'\"]*$/.test(rule[1])) { // String to Float check
          	var results = rule[1].match(/^[\"\']*((?:[\,\s]?[\-]?\d{0,3})*)([\,\.]\d{2})[\'\"]*/)
          	rule[1] = parseFloat(results[1] + results[2])

          }

          else if (/^[\"\']*([\-]?[\d]+)[\"\']*$/.test(rule[1])) { // String to Integer check
          	rule[1] = parseInt(rule[1].match(/^[\"\']*([\-]?[\d]+)[\"\']*$/)[1], 10)
          }

          else if (/[\[\]]/g.test(rule[1])) { // String to Array check
          	rule[1] = $.trim(rule[1]).replace(/[\[\]]/g, '').split('|')
          }

          else if (/^[#\.]/.test(rule[1])) { // HTML ID/Class check
          	rule[1] = rule[1]
          }

          else {
          	continue
          }

          rules[id][$.trim(rule[0])] = rule[1]
        }
      })

return rules
}

, setErrorMessages = function (data) {
	errorMessages = data.errors[locale]
}

, setupValidation = function () {

	var updateRule = function (evt, rules) {
		$(this).rules('add', rules)
	}
	, validate = function (evt) {
		var failsScreen = $(this).hasClass('hasDatepicker') || (evt.type === 'focusin' || evt.type === 'keyup') && (!$(this).hasClass('autocomplete-custom') || $(this).data('toggle') !== 'autocomplete')

		if (failsScreen) {
			return
		}

		$(this).parents('[data-validate="enable"]').validate().element(this)
	}
	, submit = function (evt) {
            //$(this).validate().form()
          }

      // override jQuery Validation with custom properties
      $.extend($.validator.prototype, prototypeOverride)
      $.extend($.validator.methods, methodsOverride)
      $.extend($.validator.messages, errorMessages)

      // set validator default properties
      $.validator.setDefaults({
      	validClass: 'form-success-inline'
      	, errorClass: 'help-inline'
		,errorElement: "span"
		,highlight:function(element, errorClass, validClass) {
			$(element).parents('.control-group').addClass('error');
		}
		,unhighlight: function(element, errorClass, validClass){
			$(element).parents('.control-group').removeClass('error');
			$(element).parents('.control-group').addClass('success');
		}
      	, keyup: true
      	, ignoreTitle: true
      	, ignore: '.ignore'

      	, rules: extractRules()
      })

      // trigger validation based on events, to account for dynamically generated forms
      $('[data-validate="enable"]')
      .on('updated.validation.rules', 'input,select', updateRule)
      .on('keyup focusin focusout change updated.dropdown', 'input,select', validate)
      .on('submit', submit)
      .validate()

      // trigger validation based on events, to account for dynamically generated forms - within modals
      $('.modal').on('shown', function () {
      	$.extend($.validator.defaults.rules, extractRules())
      	$(this)
      	.find('[data-validate="enable"]')
      	.on('updated.validation.rules', 'input,select', updateRule)
      	.on('keyup focusin focusout change updated.dropdown', 'input,select', validate)
      	.on('submit', submit)
      	.validate()
      })
    }

  // Triggers validation setup, once custom error messages are successfully called and set
  $.get('js/bootstrap/json/error-messages.json', setErrorMessages, 'json').done(setupValidation)

}(window.jQuery)